#include "WorkletRuntimeDecorator.h"
#include "JSISerializer.h"
#include "ReanimatedJSIUtils.h"
#include "Shareables.h"
#include "WorkletRuntime.h"

// RNOH PATCH
// Remove conditional inclusion of Logger.h for ANDROID
#include "Logger.h"


namespace reanimated {

    static inline double performanceNow() {
        // copied from JSExecutor.cpp
        auto time = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(time.time_since_epoch()).count();

        constexpr double NANOSECONDS_IN_MILLISECOND = 1000000.0;
        return duration / NANOSECONDS_IN_MILLISECOND;
    }

    void WorkletRuntimeDecorator::decorate(jsi::Runtime &rt, const std::string &name,
                                           const std::shared_ptr<JSScheduler> &jsScheduler) {
        // resolves "ReferenceError: Property 'global' doesn't exist at ..."
        rt.global().setProperty(rt, "global", rt.global());

        rt.global().setProperty(rt, "_WORKLET", true);

        rt.global().setProperty(rt, "_LABEL", jsi::String::createFromAscii(rt, name));

#ifdef RCT_NEW_ARCH_ENABLED
    constexpr auto isFabric = true;
#else
    constexpr auto isFabric = false;
#endif // RCT_NEW_ARCH_ENABLED
  rt.global().setProperty(rt, "_IS_FABRIC", isFabric);

#ifndef NDEBUG
  auto evalWithSourceUrl = [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args,
                              size_t count) -> jsi::Value {
      auto code = std::make_shared<const jsi::StringBuffer>(args[0].asString(rt).utf8(rt));
      std::string url;
      if (count > 1 && args[1].isString()) {
          url = args[1].asString(rt).utf8(rt);
      }
      return rt.evaluateJavaScript(code, url);
  };
  rt.global().setProperty(rt, "evalWithSourceUrl",
                          jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "evalWithSourceUrl"),
                                                                1, evalWithSourceUrl));
#endif

  jsi_utils::installJsiFunction(rt, "_toString", [](jsi::Runtime &rt, const jsi::Value &value) {
      return jsi::String::createFromUtf8(rt, stringifyJSIValue(rt, value));
  });

  jsi_utils::installJsiFunction(
      rt, "_log", [](jsi::Runtime &rt, const jsi::Value &value) { Logger::log(stringifyJSIValue(rt, value)); });

  jsi_utils::installJsiFunction(rt, "_makeShareableClone", [](jsi::Runtime &rt, const jsi::Value &value) {
      auto shouldRetainRemote = jsi::Value::undefined();
      return reanimated::makeShareableClone(rt, value, shouldRetainRemote);
  });

  jsi_utils::installJsiFunction(
      rt, "_scheduleOnJS", [jsScheduler](jsi::Runtime &rt, const jsi::Value &remoteFun, const jsi::Value &argsValue) {
          auto shareableRemoteFun = extractShareableOrThrow<ShareableRemoteFunction>(
              rt, remoteFun,
              "[Reanimated] Incompatible object passed to scheduleOnJS. It is only allowed to schedule worklets or "
              "functions defined on the React Native JS runtime this way.");
          auto shareableArgs =
              argsValue.isUndefined()
                  ? nullptr
                  : extractShareableOrThrow<ShareableArray>(rt, argsValue, "[Reanimated] Args must be an array.");
          jsScheduler->scheduleOnJS([=](jsi::Runtime &rt) {
              auto remoteFun = shareableRemoteFun->getJSValue(rt);
              if (shareableArgs == nullptr) {
                  // fast path for remote function w/o arguments
                  remoteFun.asObject(rt).asFunction(rt).call(rt);
              } else {
                  auto argsArray = shareableArgs->getJSValue(rt).asObject(rt).asArray(rt);
                  auto argsSize = argsArray.size(rt);
                  // number of arguments is typically relatively small so it is ok to
                  // to use VLAs here, hence disabling the lint rule
                  jsi::Value args[argsSize]; // NOLINT(runtime/arrays)
                  for (size_t i = 0; i < argsSize; i++) {
                      args[i] = argsArray.getValueAtIndex(rt, i);
                  }
                  remoteFun.asObject(rt).asFunction(rt).call(rt, args, argsSize);
              }
          });
      });

  jsi_utils::installJsiFunction(
      rt, "_scheduleOnRuntime",
      [](jsi::Runtime &rt, const jsi::Value &workletRuntimeValue, const jsi::Value &shareableWorkletValue) {
          reanimated::scheduleOnRuntime(rt, workletRuntimeValue, shareableWorkletValue);
      });

  jsi_utils::installJsiFunction(
      rt, "_updateDataSynchronously",
      [](jsi::Runtime &rt, const jsi::Value &synchronizedDataHolderRef, const jsi::Value &newData) {
          return reanimated::updateDataSynchronously(rt, synchronizedDataHolderRef, newData);
      });

  jsi_utils::installJsiFunction(rt, "_getDataSynchronously",
                                [](jsi::Runtime &rt, const jsi::Value &synchronizedDataHolderRef) {
                                    return reanimated::getDataSynchronously(rt, synchronizedDataHolderRef);
                                });

  jsi::Object performance(rt);
  performance.setProperty(
      rt, "now",
      jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "now"), 0,
                                            [](jsi::Runtime &runtime, const jsi::Value &, const jsi::Value *args,
                                               size_t count) { return jsi::Value(performanceNow()); }));
  rt.global().setProperty(rt, "performance", performance);
    }

} // namespace reanimated
